//
// Created by martin on 1/27/22.
//

#if 0
#include "TimeZone.h"
#include "Types.h"
#include "Date.h"
#include "noncopyable.h"

#include <string>
#include <vector>
#include <algorithm>
#include <stdexcept>
#include <assert.h>
#include <endian.h>

#include <stdio.h>
#include <stdint.h>

namespace muduo
{
const int kSecondsPerDay = 24 * 60 * 60;
namespace detail
{

struct Transition
{
    time_t gmttime;
    time_t localtime;
    int localtimeIdx;

    Transition(time_t t, time_t l, int localIdx)
    : gmttime(t), localtime(l), localtimeIdx(localIdx)
    { }
};

/**
 * Comparator for Transition
 */
struct Comp
{
    bool compareGmt; // 1: compare with GMT time; 0: compare with local time

    Comp(bool gmt)
    : compareGmt(gmt)
    { }

    bool operator()(const Transition& lhs, const Transition& rhs) const
    {
        if (compareGmt)
            return lhs.gmttime < rhs.gmttime;
        else
            return lhs.localtime < rhs.localtime;
    }

    bool equal(const Transition& lhs, const Transition& rhs) const
    {
        if (compareGmt)
            return lhs.gmttime == rhs.gmttime;
        else
            return lhs.localtime == rhs.localtime;
    }
};

struct Localtime
{
    time_t gmtOffset;
    bool isDst;
    int arrbIdx;

    Localtime(time_t offset, bool dst, int arrb)
    : gmtOffset(offset), isDst(dst), arrbIdx(arrb)
    { }
};

/**
 * fill hour, minute, second of struct tm object which pointer utc pointing to with duration (seconds)
 * @param seconds duration, unit is second
 * @param utc point to a struct tm object
 */
inline void fillHMS(unsigned seconds, struct tm* utc)
{
    utc->tm_sec = static_cast<int>(seconds % 60);
    unsigned minutes = seconds / 60;
    utc->tm_min = static_cast<int>(minutes % 60);
    utc->tm_hour = static_cast<int>(minutes / 60);
}

} // namespace detail
} // namespace muduo

using namespace muduo;
using namespace std;

struct TimeZone::Data
{
    vector<detail::Transition> transitions;
    vector<detail::Localtime> localtimes;
    vector<string> names;
    string abbreviation;
};

namespace muduo
{
namespace detail
{

class File : public noncopyable
{
public:
    File(const char* file)
    : fp_(::fopen(file, "rb"))
    {
    }

    ~File()
    {
        if (fp_)
        {
            ::fclose(fp_);
        }
    }

    bool valid() const { return fp_; }
    
    string readBytes(int n)
    {
        unsigned long t = static_cast<unsigned long>(n);
        char buf[t];
        ssize_t nr = static_cast<ssize_t>(::fread(buf, 1, t, fp_));
        if (nr != n)
            throw logic_error("no enough data");
        return string(buf, t);
    }

    int32_t readInt32()
    {
        int32_t x = 0;
        ssize_t nr = static_cast<ssize_t>(::fread(&x, 1, sizeof(int32_t), fp_));
        if (nr != sizeof(int32_t))
            throw logic_error("bad int32_t data");
        int32_t res = static_cast<int32_t >(be32toh(x));
        return res; // identical to ntohl
    }

    uint8_t readUInt8()
    {
        uint8_t x = 0;
        ssize_t nr = static_cast<ssize_t>(::fread(&x, 1, sizeof(uint8_t), fp_));
        if (nr != sizeof(uint8_t))
            throw logic_error("bad uint8_t data");
        return x;
    }

private:
    FILE* fp_;
};

/**
 * read timezone info from TimeZone file
 * @details
 * TZif format see
 * https://tools.ietf.org/id/draft-murchison-tzdist-tzif-00.html#rfc.section.3
 * @param zonefile i.e. New_York Timezone file path in Linux: /usr/share/zoneinfo/America/New_York
 * @param data
 * @return
 */
bool readTimeZoneFile(const char* zonefile, struct TimeZone::Data* data)
{
    File f(zonefile); // RAII file
    if (f.valid())
    {
        try
        {
            string head = f.readBytes(4); // magic
            if (head != "TZif")
                throw logic_error("bad head");
            string version = f.readBytes(1); // version
            f.readBytes(15); // reserved byte

            int32_t isgmtcnt = f.readInt32(); // isutccnt
            int32_t isstdcnt = f.readInt32(); // isstdcnt
            int32_t leapcnt = f.readInt32();  // leapcnt
            int32_t timecnt = f.readInt32();  // timecnt
            int32_t typecnt = f.readInt32();  // typecnt
            int32_t charcnt = f.readInt32();  // charcnt

            vector<int32_t> trans;
            vector<int> localtimes; // transition time index: timecnt
            trans.reserve(static_cast<unsigned long>(timecnt));
            for (int i = 0; i < timecnt; ++i)
            { // transition times          (timecnt x 4)
                trans.push_back(f.readInt32());
            }

            for (int i = 0; i < timecnt; ++i)
            { // local time type records   (typecnt x 6)
                uint8_t local = f.readUInt8();
                localtimes.push_back(local);
            }

            for (int i = 0; i < typecnt; ++i)
            { // local time type records   (typecnt x 6)
                int32_t gmtoff = f.readInt32();
                uint8_t isdst = f.readUInt8();
                uint8_t abbrind = f.readUInt8();

                data->localtimes.emplace_back(gmtoff, isdst, abbrind);
            }

            for (size_t i = 0; i < static_cast<size_t>(timecnt); ++i)
            { // transition time index     (timecnt)
                size_t localIdx = static_cast<size_t>(localtimes[i]);
                time_t localtime = trans[i] + data->localtimes[localIdx].gmtOffset;
//                data->transitions.emplace_back(trans[i], localtime, localIdx);
                data->transitions.push_back(Transition(trans[i], localtime, static_cast<int>(localIdx)));
            }

            // time zone designations    (charcnt)
            data->abbreviation = f.readBytes(charcnt);

            for (int i = 0; i < leapcnt; ++i)
            { // leap second records       (leapcnt x 8)
                // int32_t leaptime = f.readInt32();
                // int32_t cumleap = f.readInt32();
            }

#if 0
            for (int i = 0; i < isstdcnt; ++i)
            { // standard/wall indicators  (isstdcnt)
                uint8_t stdwalltime = f.readUInt8();
            }

            for (int i = 0; i < isgmtcnt; ++i)
            { // UTC/local indicators      (isutccnt)
                uint8_t utclocal = f.readUInt8();
            }
#endif

            // FIXME
            (void)isstdcnt;
            (void)isgmtcnt;
        }
        catch (logic_error& e)
        {
            fprintf(stderr, "%s\n", e.what());
        }
    }
    return true;
}

const Localtime* findLocaltime(const TimeZone::Data& data, Transition sentry, Comp comp)
{
    const Localtime* local = NULL;

    if (data.transitions.empty() || comp(sentry, data.transitions.front()))
    {
        // FIXME: should be first non dst time zone
        local = &data.localtimes.front();
    }
    else
    {
        // find out iterator transI pointing to data.transitions , make it established: sentry <= *transI
        vector<Transition>::const_iterator transI = lower_bound(data.transitions.begin(),
                                                                data.transitions.end(),
                                                                sentry,
                                                                comp);
        if (transI != data.transitions.end())
        { // success to find out iterator transI
            if (!comp.equal(sentry, *transI))
            {
                assert(transI != data.transitions.begin());
                --transI;
            }
            local = &data.localtimes[transI->localtimeIdx];
        }
        else
        { // fail to find out iterator transI
            // FIXME: use TZ-env
            local = &data.localtimes[data.transitions.back().localtimeIdx];
        }
    }
    return local;
}

} // namespace detail
} // namespace muduo


TimeZone::TimeZone(const char* zonefile)
: data_(new TimeZone::Data)
{
    if (!detail::readTimeZoneFile(zonefile, data_.get()))
    {
        data_.reset();
    }
}

TimeZone::TimeZone(int eastOfUtc, const char *name)
: data_(new TimeZone::Data)
{
    data_->localtimes.emplace_back(eastOfUtc, false, 0);
    data_->abbreviation = name;
}

struct tm TimeZone::toLocalTime(time_t seconds) const
{
    struct tm localTime;
    memZero(&localTime, sizeof(localTime));
    assert(data_ != NULL);
    const Data& data(*data_);

    detail::Transition sentry(seconds, 0, 0);
    const detail::Localtime* local = findLocaltime(data, sentry, detail::Comp(true));

    if (local)
    {
        time_t localSeconds = seconds + local->gmtOffset;
        ::gmtime_r(&localSeconds, &localTime); // FIXME: fromUtcTime
        localTime.tm_isdst = local->isDst;
        localTime.tm_gmtoff = local->gmtOffset;
        localTime.tm_zone = &data.abbreviation[local->arrbIdx];
    }
    return localTime;
}

time_t TimeZone::fromLocalTime(const struct tm& localTm) const
{
    assert(data_ != NULL);
    const Data& data(*data_);

    struct tm tmp = localTm;
    time_t seconds = ::timegm(&tmp); // FIXME: toUtcTime
    detail::Transition sentry(0, seconds, 0);
    const detail::Localtime* local = findLocaltime(data, sentry, detail::Comp(false));
    if (localTm.tm_isdst)
    { // DST
        struct tm tryTm = toLocalTime(seconds - local->gmtOffset);
        if (!tryTm.tm_isdst
        && tryTm.tm_hour == localTm.tm_hour
        && tryTm.tm_min == localTm.tm_min)
        {
            // FIXME: HACK
            seconds -= 3600; // 1 hour = 3600 second
        }
    }
    return seconds - local->gmtOffset;
}

struct tm TimeZone::toUtcTime(time_t secondsSinceEpoch, bool yday)
{
    struct tm utc;
    memZero(&utc, sizeof(utc));
    utc.tm_zone = "GMT";
    int seconds = static_cast<int>(secondsSinceEpoch % kSecondsPerDay);
    int days = static_cast<int>(secondsSinceEpoch / kSecondsPerDay);
    if (seconds < 0)
    {
        seconds += kSecondsPerDay;
        --days;
    }
    detail::fillHMS(static_cast<unsigned int>(seconds), &utc);
    Date date(days + Date::kJulianDayOf1970_01_01);
    Date::YearMonthDay ymd = date.yearMonthDay();
    utc.tm_year = ymd.year - 1900;
    utc.tm_mon = ymd.month - 1;
    utc.tm_mday = ymd.day;
    utc.tm_wday = date.weekDay();

    if (yday)
    {
        Date startOfYear(ymd.year, 1, 1);
        utc.tm_yday = date.julianDayNumber() - startOfYear.julianDayNumber();
    }
    return utc;
}

time_t TimeZone::fromUtcTime(const struct tm& utc)
{
    return fromUtcTime(utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday, utc.tm_hour,
                       utc.tm_min, utc.tm_sec);
}

time_t TimeZone::fromUtcTime(int year, int month, int day, int hour, int minute, int seconds)
{
    Date date(year, month, day);
    int secondsInDay = hour * 3600 + minute * 60 + seconds;
    time_t days = date.julianDayNumber() - Date::kJulianDayOf1970_01_01;
    return days * kSecondsPerDay + secondsInDay;
}

#else

#include "muduo/base/TimeZone.h"
#include "muduo/base/noncopyable.h"
#include "muduo/base/Date.h"

#include <algorithm>
#include <stdexcept>
#include <string>
#include <vector>

#include <assert.h>
//#define _BSD_SOURCE
#include <endian.h>

#include <stdint.h>
#include <stdio.h>

namespace muduo
{
namespace detail
{

struct Transition
{
    time_t gmttime;
    time_t localtime;
    int localtimeIdx;

    Transition(time_t t, time_t l, int localIdx)
            : gmttime(t), localtime(l), localtimeIdx(localIdx)
    { }

};

struct Comp
{
    bool compareGmt;

    Comp(bool gmt)
            : compareGmt(gmt)
    {
    }

    bool operator()(const Transition& lhs, const Transition& rhs) const
    {
        if (compareGmt)
            return lhs.gmttime < rhs.gmttime;
        else
            return lhs.localtime < rhs.localtime;
    }

    bool equal(const Transition& lhs, const Transition& rhs) const
    {
        if (compareGmt)
            return lhs.gmttime == rhs.gmttime;
        else
            return lhs.localtime == rhs.localtime;
    }
};

struct Localtime
{
    time_t gmtOffset;
    bool isDst;
    int arrbIdx;

    Localtime(time_t offset, bool dst, int arrb)
            : gmtOffset(offset), isDst(dst), arrbIdx(arrb)
    { }
};

inline void fillHMS(unsigned seconds, struct tm* utc)
{
    utc->tm_sec = seconds % 60;
    unsigned minutes = seconds / 60;
    utc->tm_min = minutes % 60;
    utc->tm_hour = minutes / 60;
}

}  // namespace detail
const int kSecondsPerDay = 24*60*60;
}  // namespace muduo

using namespace muduo;
using namespace std;

struct TimeZone::Data
{
    vector<detail::Transition> transitions;
    vector<detail::Localtime> localtimes;
    vector<string> names;
    string abbreviation;
};

namespace muduo
{
namespace detail
{

class File : noncopyable
{
public:
    File(const char* file)
            : fp_(::fopen(file, "rb"))
    {
    }

    ~File()
    {
        if (fp_)
        {
            ::fclose(fp_);
        }
    }

    bool valid() const { return fp_; }

    string readBytes(int n)
    {
        char buf[n];
        ssize_t nr = ::fread(buf, 1, n, fp_);
        if (nr != n)
            throw logic_error("no enough data");
        return string(buf, n);
    }

    int32_t readInt32()
    {
        int32_t x = 0;
        ssize_t nr = ::fread(&x, 1, sizeof(int32_t), fp_);
        if (nr != sizeof(int32_t))
            throw logic_error("bad int32_t data");
        return be32toh(x);
    }

    uint8_t readUInt8()
    {
        uint8_t x = 0;
        ssize_t nr = ::fread(&x, 1, sizeof(uint8_t), fp_);
        if (nr != sizeof(uint8_t))
            throw logic_error("bad uint8_t data");
        return x;
    }

private:
    FILE* fp_;
};

bool readTimeZoneFile(const char* zonefile, struct TimeZone::Data* data)
{
    File f(zonefile);
    if (f.valid())
    {
        try
        {
            string head = f.readBytes(4);
            if (head != "TZif")
                throw logic_error("bad head");
            string version = f.readBytes(1);
            f.readBytes(15);

            int32_t isgmtcnt = f.readInt32();
            int32_t isstdcnt = f.readInt32();
            int32_t leapcnt = f.readInt32();
            int32_t timecnt = f.readInt32();
            int32_t typecnt = f.readInt32();
            int32_t charcnt = f.readInt32();

            vector<int32_t> trans;
            vector<int> localtimes;
            trans.reserve(timecnt);
            for (int i = 0; i < timecnt; ++i)
            {
                trans.push_back(f.readInt32());
            }

            for (int i = 0; i < timecnt; ++i)
            {
                uint8_t local = f.readUInt8();
                localtimes.push_back(local);
            }

            for (int i = 0; i < typecnt; ++i)
            {
                int32_t gmtoff = f.readInt32();
                uint8_t isdst = f.readUInt8();
                uint8_t abbrind = f.readUInt8();

                data->localtimes.push_back(Localtime(gmtoff, isdst, abbrind));
            }

            for (int i = 0; i < timecnt; ++i)
            {
                int localIdx = localtimes[i];
                time_t localtime = trans[i] + data->localtimes[localIdx].gmtOffset;
                data->transitions.push_back(Transition(trans[i], localtime, localIdx));
            }

            data->abbreviation = f.readBytes(charcnt);

            // leapcnt
            for (int i = 0; i < leapcnt; ++i)
            {
                // int32_t leaptime = f.readInt32();
                // int32_t cumleap = f.readInt32();
            }
            // FIXME
            (void) isstdcnt;
            (void) isgmtcnt;
        }
        catch (logic_error& e)
        {
            fprintf(stderr, "%s\n", e.what());
        }
    }
    return true;
}

const Localtime* findLocaltime(const TimeZone::Data& data, Transition sentry, Comp comp)
{
    const Localtime* local = NULL;

    if (data.transitions.empty() || comp(sentry, data.transitions.front()))
    {
        // FIXME: should be first non dst time zone
        local = &data.localtimes.front();
    }
    else
    {
        vector<Transition>::const_iterator transI = lower_bound(data.transitions.begin(),
                                                                data.transitions.end(),
                                                                sentry,
                                                                comp);
        if (transI != data.transitions.end())
        {
            if (!comp.equal(sentry, *transI))
            {
                assert(transI != data.transitions.begin());
                --transI;
            }
            local = &data.localtimes[transI->localtimeIdx];
        }
        else
        {
            // FIXME: use TZ-env
            local = &data.localtimes[data.transitions.back().localtimeIdx];
        }
    }

    return local;
}

}  // namespace detail
}  // namespace muduo


TimeZone::TimeZone(const char* zonefile)
        : data_(new TimeZone::Data)
{
    if (!detail::readTimeZoneFile(zonefile, data_.get()))
    {
        data_.reset();
    }
}

TimeZone::TimeZone(int eastOfUtc, const char* name)
        : data_(new TimeZone::Data)
{
    data_->localtimes.push_back(detail::Localtime(eastOfUtc, false, 0));
    data_->abbreviation = name;
}

struct tm TimeZone::toLocalTime(time_t seconds) const
{
    struct tm localTime;
    memZero(&localTime, sizeof(localTime));
    assert(data_ != NULL);
    const Data& data(*data_);

    detail::Transition sentry(seconds, 0, 0);
    const detail::Localtime* local = findLocaltime(data, sentry, detail::Comp(true));

    if (local)
    {
        time_t localSeconds = seconds + local->gmtOffset;
        ::gmtime_r(&localSeconds, &localTime); // FIXME: fromUtcTime
        localTime.tm_isdst = local->isDst;
        localTime.tm_gmtoff = local->gmtOffset;
        localTime.tm_zone = &data.abbreviation[local->arrbIdx];
    }

    return localTime;
}

time_t TimeZone::fromLocalTime(const struct tm& localTm) const
{
    assert(data_ != NULL);
    const Data& data(*data_);

    struct tm tmp = localTm;
    time_t seconds = ::timegm(&tmp); // FIXME: toUtcTime
    detail::Transition sentry(0, seconds, 0);
    const detail::Localtime* local = findLocaltime(data, sentry, detail::Comp(false));
    if (localTm.tm_isdst)
    {
        struct tm tryTm = toLocalTime(seconds - local->gmtOffset);
        if (!tryTm.tm_isdst
            && tryTm.tm_hour == localTm.tm_hour
            && tryTm.tm_min == localTm.tm_min)
        {
            // FIXME: HACK
            seconds -= 3600;
        }
    }
    return seconds - local->gmtOffset;
}

struct tm TimeZone::toUtcTime(time_t secondsSinceEpoch, bool yday)
{
    struct tm utc;
    memZero(&utc, sizeof(utc));
    utc.tm_zone = "GMT";
    int seconds = static_cast<int>(secondsSinceEpoch % kSecondsPerDay);
    int days = static_cast<int>(secondsSinceEpoch / kSecondsPerDay);
    if (seconds < 0)
    {
        seconds += kSecondsPerDay;
        --days;
    }
    detail::fillHMS(seconds, &utc);
    Date date(days + Date::kJulianDayOf1970_01_01);
    Date::YearMonthDay ymd = date.yearMonthDay();
    utc.tm_year = ymd.year - 1900;
    utc.tm_mon = ymd.month - 1;
    utc.tm_mday = ymd.day;
    utc.tm_wday = date.weekDay();

    if (yday)
    {
        Date startOfYear(ymd.year, 1, 1);
        utc.tm_yday = date.julianDayNumber() - startOfYear.julianDayNumber();
    }
    return utc;
}

time_t TimeZone::fromUtcTime(const struct tm& utc)
{
    return fromUtcTime(utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday,
                       utc.tm_hour, utc.tm_min, utc.tm_sec);
}

time_t TimeZone::fromUtcTime(int year, int month, int day,
                             int hour, int minute, int seconds)
{
    Date date(year, month, day);
    int secondsInDay = hour * 3600 + minute * 60 + seconds;
    time_t days = date.julianDayNumber() - Date::kJulianDayOf1970_01_01;
    return days * kSecondsPerDay + secondsInDay;
}

#endif